跳到主要内容

SpringCloud Gateway

Gateway 是什么?

API 网关是所有请求的入口,承载了所有的流量,API Gateway 是一个门户一样,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facet模式(外观)很像。(说白了就是微服务太多了,得有个提供统一的接口)

API Gateway 封装内部系统的架构,并且提供API给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等

下图表示,没有网关的情况,客户端的请求会直接落到后端的各个服务中,无法集中统一管理。

下图表示,有网关的情况,所有的请求都先经过网关,然后进行分发到对应服务

API 网关的重要性

API 网关在微服务项目中是很重要的,网关提供一个统一的管理,服务间的调度变得有序

例子介绍了一个庞杂的电商系统,按照微服务理论进行设计,有如下各种服务:

  • 购物车服务:购物车中的物品数量
  • 订单服务:订单历史记录
  • 目录服务:基本产品信息,例如其名称,图像和价格
  • 审核服务:客户审核
  • 库存服务:库存不足警告
  • 运送服务:运送选项,期限和费用与运送提供商的API分开提取
  • 推荐服务:建议项目

在不使用网关的情况,客户端直接调用各服务:

理想情况,各服务调用是可以正常使用的,但是随着业务拓展,服务之间的调用越来越复杂,到时候系统就会变成如图:

如果没有一个统一的管理,肯定是不合理的,所以可以引入网关,作为一个统一的门户,如图:

API Gateway 的常见用途

动态路由

网关可以做路由转发,假如服务信息变了,只要改网关配置既可,所以说网关有动态路由(Dynamic Routing)的作用,如图:

请求监控

请求监控可以对整个系统的请求进行监控,详细地记录请求响应日志,如图,可以将日志丢到消息队列,如果没有使用网关的话,记录请求信息需要在各个服务中去做

认证鉴权

认证鉴权可以对每一个访问请求做认证,拒绝非法请求,保护后端的服务,不需要每个服务都做鉴权,在项目中经常有加上 OAuth2.0、JWT,Spring Security进行权限校验

压力测试

有网关的系统,如果要要对某个服务进行压力测试,可以如图所示,改下网关配置既可,测试请求路由到测试服务,测试服务会有单独的测试数据库,这样测试的请求就不会影响到正式的服务和数据库

三大核心概念

Route 路由:路由是构建网关的基本模块,它由ID,目标URI,一系列的断言 Predicates和过滤器 Filters组成,如果断言为true,则匹配该路由。

Predicate 断言:开发人员可以匹配 HTTP请求中的所有内容,例如请求头或请求参数,如果请求与断言相匹配则进行路由。

Filter 过滤:Spring 框架中 GatewayFilter的实例,使用过滤器,可以载请求被路由前或者后对请求进行修改。

Gateway 的工作流程

1 客户端向Spring Cloud Gateway发出请求,请求会发送到网关,DispatcherHandler 是 Http 请求的中央分发器,将请求匹配到相应的 HandlerMapping

2 请求和处理器之间有一个映射关系,网关将会对请求与路由匹配,即 handler 会通过 RoutePredicateHandlerMapping,以匹配到对应的 Route

3 经过 RoutePredicateHandlerMapping 处理后,请求会发送到 Web处理器,该 WebHandler代理了一系列网关过滤器和全局过滤,此时会对请求或者响应头进行处理

4 最后转发到具体的代理服务

注意:上图过滤器之间用虚线分开是因为过滤器可能会发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑,例如 Filter 在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 “post” 类型的过滤器中可以做响应内容、响应头的修改,志的输出,流量监控等操作。

简单的配置使用

首先引入网关的依赖

<!-- 注意,网关无需引入 spring-boot-starter-web 依赖,否则会报错 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

不要在网关引入 spring-boot-starter-web 依赖,因为它内部使用的是 webflux

在 yml 上配置路由

server:
port: 9527

spring:
application:
name: "cloud-gateway"
cloud:
gateway:
routes: # 可以配置多个路由
- id: payment_route # 路由的ID,没有固定规则但要求唯一,简易配合服务名
uri: http://localhost:8001 # 匹配的路由地址
#uri: lb://provider-service # 匹配的路由地址(lb 表示通过名字取得服务)
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由(后面的 ** 通配符可以用来匹配 PathVariable)

- id: payment_route2 # 路由的ID,没有固定规则但要求唯一,简易配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址
#uri: lb://provider-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,匹配访问这个 Gateway 的路径
#- After=2020-03-15T15:35:07.412+08:00[GMT+08:00]
#- Cookie=username,alsritter
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.alsritter.com
#- Method=GET
#- Query=username, \d+ # 要有参数名username并且值还要啥整数才能路由

启动类

@SpringBootApplication
public class GatewayGateway9527Application {

public static void main(String[] args) {
SpringApplication.run(GatewayGateway9527Application.class, args);
}

}

使用这个网关去访问,就可以直接访问到映射的目的服务了

上面 yml 配置的就是对这个服务路径的断言,路径相匹配的进行路由

使用硬编码的方式配置网关

上面使用的是 yml 文件的方式来配置网关,但是 Gateway 也提供使用代码的方式配置

只需编写一个 Bean 就行了,下面配置两个路由示例

@Configuration
public class GateWayConfig {
/**
* 当访问地址 http://localhost:9527/archives 时会自动转发到地址:https://alsritter.icu/archives/
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("payment_route3", // 路由的ID,没有固定规则但要求唯一,简易配合服务名
r -> r.path("/payment/discovery") // 断言,路径相匹配的进行路由
.uri("http://localhost:8001")); // 匹配后提供服务的路由地址

// 再配置一个映射到博客的
routes.route("payment_route4",
r -> r.path("/archives/**")
.uri("https://alsritter.icu/")); // 匹配后提供服务的路由地址

return routes.build();
}
}

配置动态路由

上面的地址都是写死了,但是实际微服务开发应该使用的是服务名来做路由地址,所以需要在网关这里使用负载均衡

首先将其注册进服务中心

<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka #单机版
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayGateway9527Application {

public static void main(String[] args) {
SpringApplication.run(GatewayGateway9527Application.class, args);
}

}

然后开启从注册中心动态创建路由的功能,利用微服务名进行路由

spring:
application:
name: "cloud-gateway"
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由

这时在把上面的 uri 地址改一下,注意,协议名不再是 http 而是 li

# uri: http://localhost:8001         # 匹配后提供服务的路由地址
uri: lb://PAYMENT-SERVICE # 匹配后提供服务的路由地址

然后 Gateway 就会自动配置负载均衡了

常用的 Predicate

全部配置参考 官方文档 Route Predicate Factories 这里转载自 【SpringCloud】Gateway常用的Predicate(十八)

Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分进行匹配。

Spring Cloud Gateway 包括许多内置的路由断言工厂。所有这些断言都与 HTTP 请求的不同属性匹配,将匹配到的 url 路由给指定的微服务。可以将多个路由断言工厂与逻辑 and 语句结合使用。

spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8001
predicates:
- Cookie=username, alsritter

如上,这个匹配到的 url,被路由到 http://localhost:8001 这个微服务里面

After Route Predicate

所述 After 断言有一个参数,一个 datetime(其是Java ZonedDateTime)。该断言匹配在指定日期时间之后发生的请求。下面的示例配置路由后断言:

spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://localhost:80017
predicates:
- After=2020-04-20T23:57:57.308+08:00[Asia/Shanghai]

这路由符合2020年4月20日23:57时区时间(上海)之后的任何请求

Before Route Predicate

所述 Before 断言有一个参数,一个datetime(其是Java ZonedDateTime)。该断言匹配在指定之前发生的请求 datetime。以下示例配置了路由前断言:

spring:
cloud:
gateway:
routes:
- id: before_route
uri: http://localhost:80017
predicates:
- Before=2020-04-21T23:57:57.308+08:00[Asia/Shanghai]

这路由符合2020年4月21日23:57时区时间(上海)之前的任何请求。

Between Route Predicate

该 Between 断言有两个参数,datetime1 并且 datetime2 这是 Java ZonedDateTime对象。该断言匹配在之后 datetime1和之前发生的请求 datetime2。该 datetime2参数必须是 datetime1 后面。以下示例配置了路由之间的断言:

spring:
cloud:
gateway:
routes:
- id: between_route
uri: http://localhost:8001
predicates:
- Between=2020-04-20T23:57:57.308+08:00[Asia/Shanghai], 2020-04-21T23:57:57.308+08:00[Asia/Shanghai]

所述 Cookie 断言采用两个参数,该 cookie name和 regexp(其是Java正则表达式)。该断言匹配具有给定名称且其值与正则表达式匹配的cookie。以下示例配置Cookie路由断言:

spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8001
predicates:
- Cookie=username, alsritter

测试请求命令:

curl http://localhost:9527/payment/get/1 --cookie "username=alsritter"

Header Route Predicate

所述Header断言采用两个参数,报头name和一个regexp(其是Java正则表达式)。该断言与具有给定名称且其值与正则表达式匹配的标头匹配。以下示例配置标头路由断言:

spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://localhost:8001
predicates:
- Header=X-Request-Id, \d+

如果请求具有名为X-Request-Id其值与 \d+ 正则表达式匹配的标头(即,其值为一个或多个数字),则此路由匹配。

可以使用测试请求命令:

curl http://localhost:9527/payment/get/1 -H "X-Request-Id:123"

Host Route Predicate

该 Host 断言需要一个参数:主机名的列表 patterns。该模式是带有 . 分隔符的 Ant样式的模式。断言与 Host匹配模式的标头匹配。以下示例配置主机路由断言:

spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://localhost:8001
predicates:
- Host=**.x.com

如果请求具有这种路由匹配 Host用的头值 **.x.com

测试请求命令:

curl http://localhost:9527/payment/get/1 -H "Host:demo1.x.com"

Method Route Predicate

所述 Method 断言需要 methods 的参数,它是一个或多个参数:HTTP方法来匹配。以下示例配置方法路由断言:

spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://localhost:8001
predicates:
- Method=GET

测试请求命令:

curl http://localhost:9527/payment/get/1 -X GET

Path Route Predicate

spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://localhost:8001
predicates:
- Path=/payment/get/**

Query Route Predicate

所述 Query 断言采用两个参数:所要求的param和可选的regexp(其是Java正则表达式)。以下示例配置查询路由断言:

spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://localhost:8001
predicates:
- Query=green

如果请求包含green查询参数,则前面的路由匹配。

测试请求命令:

curl http://localhost:9527/payment/get/1?green=1

RemoteAddr Route Predicate

所述 RemoteAddr 断言需要的列表 sources,其是 CIDR的表示法(IPv4或IPv6)的字符串,如 192.168.0.1/16(其中 192.168.0.1 是一个IP地址和16一个子网掩码)。以下示例配置一个 RemoteAddr 路由谓词:

spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://localhost:8001
predicates:
- RemoteAddr=192.168.1.1/24

请求测试命令:

curl http://192.168.1.4:9527/payment/get/1

Weight Route Predicate

该 Weight断言有两个参数:group和weight(一个int)。权重是按组计算的。以下示例配置权重路由断言:

spring:
gateway:
discovery:
routes:
- id: weight_high
uri: http://localhost:8001
predicates:
- Weight=group1, 8
- id: weight_low
uri: http://localhost:8002
predicates:
- Weight=group1, 2

这条路线会将大约 80%的流量转发到 http://localhost:8001,将大约20%的流量转发到 http://localhost:8002

Filter 过滤器

转载自 spring cloud gateway之filter篇

由 Gateway 工作流程,可以知道 filter 有着非常重要的作用,在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 “post” 类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。

当我们有很多个服务时,比如下图中的 user-service、goods-service、sales-service 等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。

对于这样重复的工作,可以在微服务的上一层加一个全局的权限控制、限流、日志输出的 Api Gatewat 服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。

生命周期

Spring Cloud Gateway 同 zuul类似,有 “pre” 和 “post” 两种方式的 filter。客户端的请求先经过 “pre” 类型的 filter,然后将请求转发到具体的业务服务,比如上图中的 user-service,收到业务服务的响应之后,再经过 “post” 类型的 filter 处理,最后返回响应到客户端。

与 zuul不同的是,filter除了分为 “pre” 和 “post” 两种方式的 filter外,在 Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的 gateway filter,它在配置文件中的写法同 predict 类似;另外一种是针对于所有路由的 global gateway filer。

使用自带的 Filter

全部配置参考 官方文档 GatewayFilter Factories 参考资料 Spring cloud gateway 详解和配置使用(文章较长)

Route filters 可以通过一些方式修改 HTTP请求的输入和输出,针对某些特殊的场景,Spring Cloud Gateway已经内置了很多不同功能的 GatewayFilter Factories。

这里就只记录常用的几个,具体的看文档

AddRequestHeader

AddRequestHeader GatewayFilter Factory 通过配置name和value可以增加请求的header。

spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-Foo, Bar

对匹配的请求,会额外添加 X-Request-Foo:Bar 的 header。

AddRequestParameter

AddRequestParameter GatewayFilter Factory 通过配置name和value可以增加请求的参数。

spring:  
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: http://www.google.com
filters:
- AddRequestParameter=foo, bar

对匹配的请求,会额外添加 foo=bar 的请求参数。

AddResponseHeader

AddResponseHeader GatewayFilter Factory 通过配置name和value可以增加响应的header

spring:  
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://www.google.com
filters:
- AddResponseHeader=X-Response-Foo, Bar

对匹配的请求,响应返回时会额外添加 X-Response-Foo:Bar 的header返回。

自定义配置一个全局 Filter

/**
* 这个 Ordered 用来控制访问顺序
**/
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("come in MyLogGateWayFilter: " + new Date());

String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//每次进来后判断带不带 uname 这个 key
if (uname == null) {
log.info("用户名为null ,非法用户");
//uname为null非法用户
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

注意过滤器执行流程如下,order 越大,优先级越低

Reference

参考资料 What is API gateway really all about? Java Brains - Brain Bytes 参考资料 SpringCloud系列之API网关(Gateway)服务Zuul 参考资料 Spring Cloud Gateway学习 参考资料 【SpringCloud】Gateway常用的Predicate(十八)